# coding: utf-8
# @Author: 莫小鱼
import json
import re
import traceback
import allure
import jsonpath as jsonpath
from AutoApiTest.unit_tools.debugtalk import DebugTalk
from AutoApiTest.unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
from AutoApiTest.unit_tools.log_util import logs
from AutoApiTest.unit_tools.handle_data.configParse import ConfigParse
from AutoApiTest.unit_tools.sendrequests import SendRequests
from AutoApiTest.unit_tools.assertion_utils import Assertions


class RequestsBase:

    def __init__(self):
        self.conf = ConfigParse()
        self.send_request = SendRequests()
        self.asserts = Assertions()

    @classmethod
    def parse_and_replace_variables(cls, yml_data):
        """
        解析并替换yaml数据中的变量引用，如：${get_extratc_data()}
        :param yml_data: 解析的yaml数据
        :return:返回的是一个字典类型
        """
        # 将传入字典格式的数据转换成字符串格式的数据
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        logs.info(f"解析前：{yml_data_str}")
        # 计算'${'这种格式的字符串出现的次数
        for _ in range(yml_data_str.count('${')):
            if '${' in yml_data_str and '}' in yml_data_str:
                start_index = yml_data_str.index('$')
                end_index = yml_data_str.index('}', start_index)

                variable_data = yml_data_str[start_index:end_index + 1]  # 通过切片获取到 ${get_now_time()}
                # logs.info(variable_data)

                # 使用正则表达式提取函数名和参数
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # logs.info(match)

                if match:
                    func_name, func_params = match.groups()

                    func_params = func_params.split(',') if func_params else []  # 通过切片获取传入的参数
                    # logs.info(func_params)

                    # 使用面向对象反射getattr调用函数 ==> 类的反射机制：类的反射是指通过 字符串形式 的类名或属性来访问
                    #                                             对应类或属性 func_name = get_now_time
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    logs.info(f'提取到的结果：{extract_data}')

                    # 使用正则表达式替换原始字符串中的变量引用为替换后的结果
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
                    # logs.info(f"解析后：{yml_data_str}")

        # 还原数据，将其转换为字典类型
        try:
            data = json.loads(yml_data_str)
        except json.JSONDecodeError:
            data = yml_data_str

        return data

    @classmethod
    def allure_attach_dict_result(cls, result):
        """
        处理结果是字典类型，就将其转换成字符串类型，并做格式化处理，否则直接返回
        :return:
        """
        if isinstance(result, dict):
            allure_response = json.dumps(result, ensure_ascii=False, indent=4)
        else:
            allure_response = result
        return allure_response

    def execute_test_case(self, base_info, testcase):
        """
        规范yaml接口信息，执行接口、提取结果以及断言操作
        :param base_info: (dict) yaml里面的接口基本信息
        :param testcase: （dict） yaml里面的接口测试用例信息
        :return:
        """

        try:
            # 处理接口 baseInfo 基本信息
            conf_host = self.conf.get_host('host')  # 获取项目地址
            url = conf_host + base_info['api_url']  # 拼接接口url地址
            logs.info(f"请求地址：{url}")

            api_name = base_info['api_name']  # 获取接口名称
            method = base_info['method']  # 获取请求方式

            headers = base_info.get('headers', None)  # 获取请求头信息，没有则返回None
            logs.info(f"未解析前的headers：{headers},数据类型：{type(headers)}")
            if headers is not None:
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                logs.info(f"解析后的headers：{headers},数据类型：{type(headers)}")

            cookies = base_info.get('cookies', None)  # 处理cookies
            logs.info(f"解析前的cookies：{cookies},数据类型：{type(cookies)}")
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                logs.info(f"解析后的cookies：{cookies},数据类型：{type(cookies)}")

            # 处理 testcase 信息
            logs.info(f"处理前的用例数据：{testcase}")
            case_name = testcase.pop('case_name')

            # 通过变量引用处理断言结果
            val_result = self.parse_and_replace_variables(testcase.get('validation'))
            testcase['validation'] = val_result
            validation = testcase.pop('validation')
            logs.info(f"处理断言后的用例数据：{testcase}")

            # 处理接口返回值提取部分
            extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
            logs.info(f"处理提取值后的用例数据：{testcase}")

            param_type, request_params = None, None
            # 处理参数类型和请求参数
            for param_type, param_value in testcase.items():
                logs.info(f"实际请求参数类型：{param_type}")

                if param_type in ['params', 'data', 'json']:
                    request_params = self.parse_and_replace_variables(param_value)
                    testcase[param_type] = request_params

                    logs.info(f"实际请求参数：{request_params}")

            # 最终处理的testcase数据
            logs.info(f"最终testcase参数：{testcase},数据类型为：{type(testcase)}")

            # 文件上传操作
            files = testcase.pop('files', None)
            if files:
                for fk, fv in files.items():
                    files = {fk: open(fv, mode='rb')}

            response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method,
                                                             headers=headers,
                                                             case_name=case_name, cookie=cookies, files=files,
                                                             **testcase)

            status_code, response_text = response.status_code, response.text
            logs.info(f"接口实际响应状态码：{status_code};实际响应结果：{response.json()}")

            # 处理allure报告test body 显示内容
            allure_info = {
                '接口地址': url,
                '接口名称': api_name,
                '请求方式': method,
                '请求头': self.allure_attach_dict_result(headers if headers else "无需请求头"),
                'Cookies': self.allure_attach_dict_result(cookies if cookies else "无需cookies"),
                '测试用例名称': case_name,
                '参数类型': param_type if param_type else "",
                '请求参数': self.allure_attach_dict_result(request_params if request_params else "无需入参"),
                '接口实际响应信息': self.allure_attach_dict_result(response.json())

            }
            for title, content in allure_info.items():
                allure.attach(content, title, attachment_type=allure.attachment_type.JSON)

            # 处理接口返回值提取
            if extract is not None:
                self.extract_data(extract, response_text)

            if extract_list is not None:
                self.extract_data_list(extract_list, response_text)

            # 处理接口断言

            logs.info(f"返回的接口的响应信息：{response.json()}")
            self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            logs.error(f"出现未知异常：--{str(traceback.format_exc())}--")
            raise e

    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数，提取接口返回参数，支持正则表达式提取和json提取
        :param testcase_extract: （dict）yaml文件中的 extract值，例如：{‘token': '$.token'}
        :param response_text: (str) 接口实际返回值
        :return:
        """
        logs.info(testcase_extract)
        extract_data = None
        try:
            for key, value in testcase_extract.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    ext_list = re.search(value, response_text)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    logs.info(f"正则提取到的结果：{extract_data}")
                elif "$" in value:
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]

                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据，请检查返回值或表达式"}
                    logs.info(f"json提取到的结果：{extract_data}")

                # 将提取到的值写入extract.yaml文件
                if extract_data:
                    write_yaml(extract_data)

        except re.error:
            logs.error("正则表达式解析错误，请检查yaml文件extract表达式")
        except json.JSONDecodeError:
            logs.error("json表达式解析错误，请检查yaml文件extract表达式")

        except Exception as e:
            logs.error(f"提取返回参数异常：--{e}--")

    @classmethod
    def extract_data_list(self, testcase_extract_list, response_text):
        """
        提取多个参数，提取接口返回参数，支持正则表达式提取和json提取
        :param testcase_extract_list: dict）yaml文件中的 extract_list值，例如：
        :param response_text: (str) 接口实际返回值
        :return:
        """
        extract_data = None
        try:
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    ext_list = re.findall(value, response_text, re.S)
                    logs.info(f"正则提取到的多个参数：{ext_list}")
                    if ext_list:
                        extract_data = {key: ext_list}

                elif "$" in value:
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data = {key: "未提取到数据，请检查接口返回信息或jsonpath表达式"}
                if extract_data:
                    logs.info(f"json提取到的多个参数：{extract_data}")
                    write_yaml(extract_data)

        except re.error:
            logs.error("正则表达式解析错误，请检查yaml文件extract表达式")
        except json.JSONDecodeError:
            logs.error("json表达式解析错误，请检查yaml文件extract表达式")

        except Exception as e:
            logs.error(f"提取多个返回参数异常：--{e}--")


if __name__ == '__main__':
    data = read_yaml('../testcase/userManager/adduser.yaml')
    print(data[0][0],data[0][1])
    req = RequestsBase()
    # res = req.parse_and_replace_variables(data)
    # logs.info(f'解析后：{res}')
    req.execute_test_case(data[0][0],data[0][1])
